// code by chatgpt

use openssl::base64;
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use openssl::sign::Signer;
use rand::Rng;
use reqwest::{Client, Error, StatusCode};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use std::error::Error as stdEr;
use std::fs::File;
use std::io::Read;

use crate::bootstrap::wechat::WeChatProperties;

pub struct WeChatPayUtil {
    wechat_properties: WeChatProperties,
    client: Client,
}

impl WeChatPayUtil {
    pub fn new() -> Self {
        let client = Client::new();
        let wechat_properties: WeChatProperties =
            crate::bootstrap::wechat::WeChatProperties::load();
        WeChatPayUtil {
            wechat_properties,
            client,
        }
    }

    // Load the private key
    fn load_private_key(&self) -> Rsa<openssl::pkey::Private> {
        let mut file = File::open(&self.wechat_properties.private_key_file_path)
            .expect("Cannot open private key file");
        let mut key_data = Vec::new();
        file.read_to_end(&mut key_data)
            .expect("Cannot read private key file");
        Rsa::private_key_from_pem(&key_data).expect("Invalid private key")
    }

    // Load the platform certificate
    fn load_cert(&self) -> Vec<u8> {
        let mut file = File::open(&self.wechat_properties.wechat_pay_cert_file_path)
            .expect("Cannot open WeChat Pay cert file");
        let mut cert_data = Vec::new();
        file.read_to_end(&mut cert_data)
            .expect("Cannot read WeChat Pay cert file");
        cert_data
    }

    // Method to send POST request to WeChat API
    async fn post(&self, url: &str, body: &str) -> Result<String, Error> {
        let response = self
            .client
            .post(url)
            .header("Content-Type", "application/json")
            .header("Wechatpay-Serial", &self.wechat_properties.mch_serial_no)
            .body(body.to_string())
            .send()
            .await?;

        response.text().await
    }

    // Method to send GET request to WeChat API
    async fn get(&self, url: &str) -> Result<String, Error> {
        let response = self
            .client
            .get(url)
            .header("Wechatpay-Serial", &self.wechat_properties.mch_serial_no)
            .send()
            .await?;

        response.text().await
    }

    // Helper function to generate nonce
    fn generate_nonce() -> String {
        let mut rng = rand::thread_rng();
        (0..32).map(|_| rng.gen_range('0'..'9')).collect()
    }

    // Helper function to generate timestamp
    fn generate_timestamp() -> String {
        (chrono::Utc::now().timestamp() as u64).to_string()
    }

    // JSAPI method for creating a transaction
    pub async fn jsapi(
        &self,
        order_num: &str,
        total: Decimal,
        description: &str,
        openid: &str,
    ) -> Result<String, Error> {
        let amount = json!({
            "total": total, // Amount in cents
            "currency": "CNY"
        });

        let payer = json!({
            "openid": openid
        });

        let body = json!({
            "appid": &self.wechat_properties.appid,
            "mchid": &self.wechat_properties.mchid,
            "description": description,
            "out_trade_no": order_num,
            "notify_url": &self.wechat_properties.notify_url,
            "amount": amount,
            "payer": payer
        });

        let body_str = body.to_string();

        self.post(
            "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi",
            &body_str,
        )
        .await
    }

    // Refund method
    pub async fn refund(
        &self,
        out_trade_no: &str,
        out_refund_no: &str,
        refund: Decimal,
        total: Decimal,
    ) -> Result<String, Error> {
        let refund_cents = (refund * Decimal::new(100, 0)).round();

        let total_cents = (total * Decimal::new(100, 0)).round();

        let amount = json!({
            "refund": refund_cents,
            "total": total_cents,
            "currency": "CNY"
        });

        let body = json!({
            "out_trade_no": out_trade_no,
            "out_refund_no": out_refund_no,
            "amount": amount,
            "notify_url": &self.wechat_properties.refund_notify_url
        });

        let body_str = body.to_string();

        self.post(
            "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds",
            &body_str,
        )
        .await
    }

    // Method to sign the data using RSA
    fn sign_data(&self, data: &str) -> String {
        let rsa_key = self.load_private_key();
        let pkey = PKey::from_rsa(rsa_key).expect("Failed to convert RSA to PKey");

        let mut signer =
            Signer::new(MessageDigest::sha256(), &pkey).expect("Failed to create signer");
        signer
            .update(data.as_bytes())
            .expect("Failed to update signer");
        let signature = signer.sign_to_vec().expect("Failed to sign data");
        base64::encode_block(&signature)
    }

    // Payment method to initiate a payment for mini-program
    pub async fn pay(
        &self,
        order_num: &str,
        total: Decimal,
        description: &str,
        openid: &str,
    ) -> Result<HashMap<String, String>, (StatusCode, String)> {
        let body = self
            .jsapi(order_num, total, description, openid)
            .await
            .unwrap();

        let json_object: serde_json::Value = serde_json::from_str(&body).unwrap();

        if let Some(prepay_id) = json_object["prepay_id"].as_str() {
            let timestamp = WeChatPayUtil::generate_timestamp();
            let nonce_str = WeChatPayUtil::generate_nonce();

            let sign_message = format!(
                "{}\n{}\n{}\nprepay_id={}\n",
                self.wechat_properties.appid, timestamp, nonce_str, prepay_id
            );

            let package_sign = self.sign_data(&sign_message);

            let mut response = HashMap::new();
            response.insert("timeStamp".to_string(), timestamp);
            response.insert("nonceStr".to_string(), nonce_str);
            response.insert("package".to_string(), format!("prepay_id={}", prepay_id));
            response.insert("signType".to_string(), "RSA".to_string());
            response.insert("paySign".to_string(), package_sign);

            Ok(response)
        } else {
            Err((StatusCode::BAD_REQUEST, "prepay_id not found".to_string()))
        }
    }
}
